package com.hecticant.thinpass.persistence;

import java.util.List;

import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

/**
 * A singleton that manages the default store.
 * 
 * @author Pedro Fonseca
 */
public class StoreController {
	private static final String TAG = "StoreController";
	private static final String SKEY_KEY = "SECRETKEY";
	private static final String CHAL_KEY = "CHALLENGE";
	private static final String SSAL_KEY = "SECRETSALT";
	private static final String MSAL_KEY = "MASTERSALT";
	
	private static StoreController scSingleton;
	
	private Store store;
	
	private StoreController(Object context) {
		this.store = new DefaultStore(context);
	}
	
	public static synchronized StoreController getInstance(Object context) {
		if (scSingleton == null) {
			scSingleton = new StoreController(context);
		} else {
			((DefaultStore) scSingleton.store).checkState();
		}
		return scSingleton;
	}
	
	@Override
	public Object clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException();
	}
	
	public long rowCount() {
		return store.countAccounts();
	}
	
	public List<Account> accountsInRange(long offset, long limit) {
		return store.accountsInRange(offset, limit);
	}
		
	public List<Account> accounts() {
		return ((DefaultStore) store).accounts();
	}
	
	/**
	 * Adds a new account to the store. The account id for this account is 
	 * automatically generated by the <code>Store</code>. 
	 * <p>
	 * The underlying store saves the data "as is" therefore the caller is 
	 * responsible for encrypting sensitive information before passing it 
	 * to this <code>StoreController</code>.
	 * 
	 * @param username an optional username for the account.
	 * @param password a password for the account.
	 * @param description a description for the account.
	 * @return the new account.
	 * 
	 * @see Store
	 * @see Account
	 */
	public Account addAccount(String username, byte[] password, 
			byte[] description) 
	{
		if (password == null || description == null) {
			throw new NullPointerException();
		}
		
		long count;
		try {
			count = store.countAccounts();
			store.addAccount(username, password, description);
		} 
		catch (SQLException e) {
			return null;
		}
			
		Account a = null;
		List<Account> list = accountsInRange(count, 1);
		try {
			a = list.get(0);
		}
		catch (IndexOutOfBoundsException e) {}
		return a;
	}
	
	/** 
	 * 
	 * @param acc
	 */
	public void updateAccount(Account acc) {
		if (acc == null) {
			throw new NullPointerException();
		}
		
		try {
			store.updateAccount(acc);
		} 
		catch (SQLException e) {}
	}
	
	public byte[] key() {
		return store.valueForKey(SKEY_KEY);
	}
	
	public byte[] passwordSalt() {
		return store.valueForKey(MSAL_KEY);
	}
	
	public void setPasswordSalt(byte[] salt) {
		store.setValueForKey(MSAL_KEY, salt, true);
	}
	
	public byte[] keySalt() {
		return store.valueForKey(SSAL_KEY);
	}
	
	public byte[] challenge() {
		return store.valueForKey(CHAL_KEY);
	}
	
	/**
	 * Checks if a master key exists. The following conditions must be met: 
	 * <ul>
	 * <li>The salt used to generate the key must exist in the application data 
	 * store
	 * <li>The random key, generated when the master key is created, must be 
	 * hashed in the application data store 
	 * </ul>
	 * 
	 * @return
	 * 
	 * @see #storeKey(byte[], byte[], byte[])
	 * @see #passwordSalt()
	 * @see #challenge()
	 */
	public boolean hasKey() {
		return passwordSalt() != null && challenge() != null;
	}
	
	/**
	 * Saves a new key to the store. The key to be stored is a random symmetric 
	 * key that is used to encrypt sensitive user data saved on the store. The 
	 * key itself is protected by the master key which is derivated from a 
	 * password. Thus the password can be changed without re-encrypting the 
	 * store and the random key can be changed if necessary.
	 * 
	 * <p>
	 * Master and random keys are related one-to-one. However, that can be 
	 * easily extended to a one-to-many relationship.
	 * 
	 * @param encryptedKey a key encrypted with the master key.
	 * @param salt the salt used when generating <code>check</check>
	 * @param check a cryptographic checksum to verify if the 
	 * 			<code>encryptedKey</code> is correctly decrypted. 
	 * @return if the new key was successfully stored.
	 */
	public boolean storeKey(byte[] encryptedKey, byte[] salt, byte[] check) {
		if (encryptedKey == null || check == null) {
			return false;
		}
		
		SQLiteDatabase db = ((DefaultStore) store).getRawStore();
		if (!db.isOpen()) {
			return false;
		}
		
		boolean didSet = true;
		db.beginTransaction();
		try {	
			store.setValueForKey(SKEY_KEY, encryptedKey, false);
			store.setValueForKey(CHAL_KEY, check, false);
			store.setValueForKey(SSAL_KEY, salt, false); 
			db.setTransactionSuccessful();
		} 
		catch (Exception e) {
			Log.e(TAG, e.getLocalizedMessage());
			didSet = false;
		}
		finally {
			db.endTransaction();
		}
		
		return didSet;
	}
	
	/**
	 * Updates the encrypted text of the key when the master key is changed.
	 * The key itself must remain unchanged.
	 * 
	 * @param encryptedKey
	 */
	public void updateKey(byte[] encryptedKey) {
		if (encryptedKey == null) {
			throw new NullPointerException();
		}
		store.setValueForKey(SKEY_KEY, encryptedKey, true);
	}
	
	/* (non-javadoc) @see #storeKey(byte[], byte[], byte[]) */
	public boolean replaceKey(byte[] encryptedKey, byte[] salt, byte[] check) {
		/* TO BE IMPLEMENTED */
		return false;
	}
	
	public void obliterateStore() {
		store.obliterate();
	}
	
	public void close() {
		store.close();
	}
}
